// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'dart:math' as math;

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/category_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_category_axis.dart';
import 'package:charts_grasp/src/chart/axis/logarithmic_axis.dart';
import 'package:charts_grasp/src/chart/base/chart_base.dart';
import 'package:charts_grasp/src/chart/chart_segment/chart_segment.dart';
import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/common/data_label.dart';
import 'package:charts_grasp/src/chart/common/interactive_tooltip.dart';
import 'package:charts_grasp/src/chart/common/marker.dart';
import 'package:charts_grasp/src/chart/trendlines/trendlines.dart';
import 'package:charts_grasp/src/chart/user_interaction/trackball.dart';
import 'package:charts_grasp/src/chart/user_interaction/trackball_painter.dart';
import 'package:charts_grasp/src/chart/user_interaction/trackball_template.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/common.dart';
import 'package:charts_grasp/src/common/rendering_details.dart';
import 'package:charts_grasp/src/common/series/chart_series.dart';
import 'package:charts_grasp/src/common/template/rendering.dart';
import 'package:charts_grasp/src/common/user_interaction/selection_behavior.dart';
import 'package:charts_grasp/src/common/user_interaction/tooltip.dart';
import 'package:charts_grasp/src/common/user_interaction/tooltip_rendering_details.dart';
import 'package:charts_grasp/src/common/utils/enum.dart';
import 'package:charts_grasp/src/common/utils/typedef.dart';
import 'package:flutter/material.dart';

abstract class CartesianSeries<T, D> extends ChartSeries<T, D> {
  CartesianSeries(
      {this.key,
      this.xValueMapper,
      this.yValueMapper,
      this.dataLabelMapper,
      this.name,
      required this.dataSource,
      this.xAxisName,
      this.yAxisName,
      this.sizeValueMapper,
      this.pointColorMapper,
      this.color,
      this.legendItemText,
      this.sortFieldValueMapper,
      this.gradient,
      this.borderGradient,
      this.width,
      this.highValueMapper,
      this.lowValueMapper,
      this.intermediateSumPredicate,
      this.totalSumPredicate,
      this.trendlines,
      this.onRendererCreated,
      this.onCreateRenderer,
      this.onPointTap,
      this.onPointDoubleTap,
      this.onPointLongPress,
      this.onCreateShader,
      MarkerSettings? markerSettings,
      bool? isVisible,
      super.enableTooltip,
      super.emptyPointSettings,
      super.dataLabelSettings,
      super.animationDuration,
      List<double>? dashArray,
      List<int>? initialSelectedDataIndexes,
      Color? borderColor,
      double? borderWidth,
      super.selectionBehavior,
      bool? isVisibleInLegend,
      super.legendIconType,
      super.opacity,
      super.animationDelay,
      super.sortingOrder,})
      : isVisible = isVisible ?? true,
        markerSettings = markerSettings ?? const MarkerSettings(),
        dataLabelSettings = dataLabelSettings ?? const DataLabelSettings(),
        enableTooltip = enableTooltip ?? true,
        emptyPointSettings = emptyPointSettings ?? EmptyPointSettings(),
        dashArray = dashArray ?? <double>[0, 0],
        assert(dashArray == null || dashArray.isNotEmpty, 'The dashArray list must not be empty'),
        initialSelectedDataIndexes = initialSelectedDataIndexes ?? <int>[],
        animationDuration = animationDuration ?? 1500,
        borderColor = borderColor ?? Colors.transparent,
        selectionBehavior = selectionBehavior ?? SelectionBehavior(),
        legendIconType = legendIconType ?? LegendIconType.seriesType,
        isVisibleInLegend = isVisibleInLegend ?? true,
        borderWidth = borderWidth ?? 0,
        opacity = opacity ?? 1,
        animationDelay = animationDelay ?? 0,
        sortingOrder = sortingOrder ?? SortingOrder.none,
        super(
            name: name,
            xValueMapper: xValueMapper,
            yValueMapper: yValueMapper,
            sortFieldValueMapper: sortFieldValueMapper,
            pointColorMapper: pointColorMapper,
            dataLabelMapper: dataLabelMapper,
            dataSource: dataSource,);

  final ValueKey<String>? key;

  final ChartSeriesRendererFactory<T, D>? onCreateRenderer;

  final SeriesRendererCreatedCallback? onRendererCreated;

  final ChartPointInteractionCallback? onPointTap;

  final ChartPointInteractionCallback? onPointDoubleTap;

  final ChartPointInteractionCallback? onPointLongPress;

  @override
  // ignore: overridden_fields
  final List<T> dataSource;

  @override
  // ignore: overridden_fields
  final ChartIndexedValueMapper<D>? xValueMapper;

  @override
  // ignore: overridden_fields
  final ChartIndexedValueMapper<dynamic>? yValueMapper;

  @override
  // ignore: overridden_fields
  final ChartIndexedValueMapper<Color>? pointColorMapper;

  @override
  // ignore: overridden_fields
  final ChartIndexedValueMapper<String>? dataLabelMapper;

  final ChartIndexedValueMapper<num>? sizeValueMapper;

  final ChartIndexedValueMapper<num>? highValueMapper;

  final ChartIndexedValueMapper<num>? lowValueMapper;

  final ChartIndexedValueMapper<bool>? intermediateSumPredicate;

  final ChartIndexedValueMapper<bool>? totalSumPredicate;

  final String? xAxisName;

  final String? yAxisName;

  final Color? color;

  final double? width;

  final MarkerSettings markerSettings;

  @override
  // ignore: overridden_fields
  final EmptyPointSettings emptyPointSettings;

  @override
  // ignore: overridden_fields
  final DataLabelSettings dataLabelSettings;

  final List<Trendline>? trendlines;

  final LinearGradient? gradient;

  final LinearGradient? borderGradient;

  @override
  // ignore: overridden_fields
  final String? name;

  @override
  // ignore: overridden_fields
  final bool enableTooltip;

  final List<double> dashArray;

  @override
  // ignore: overridden_fields
  final double animationDuration;

  @override
  // ignore: overridden_fields
  final Color borderColor;

  @override
  // ignore: overridden_fields
  final double borderWidth;

  @override
  // ignore: overridden_fields
  final LegendIconType legendIconType;

  final bool isVisibleInLegend;

  @override
  // ignore: overridden_fields
  final String? legendItemText;

  @override
  // ignore: overridden_fields
  final SelectionBehavior selectionBehavior;

  @override
  // ignore: overridden_fields
  final double opacity;

  @override
  // ignore: overridden_fields
  final ChartIndexedValueMapper<dynamic>? sortFieldValueMapper;

  @override
  // ignore: overridden_fields
  final SortingOrder sortingOrder;

  @override
  // ignore: overridden_fields
  final bool isVisible;

  @override
  // ignore: overridden_fields
  final double? animationDelay;

  final List<int>? initialSelectedDataIndexes;

  final CartesianShaderCallback? onCreateShader;
}

abstract class ChartSeriesRenderer {}

class ChartSeriesController {
  ChartSeriesController(this.seriesRenderer);

  final XyDataSeriesRenderer seriesRenderer;

  bool _needXRecalculation = false;
  bool _needYRecalculation = false;
  bool _needRemove = false;

  void updateDataSource(
      {List<int>? addedDataIndexes,
      List<int>? removedDataIndexes,
      List<int>? updatedDataIndexes,
      int? addedDataIndex,
      int? removedDataIndex,
      int? updatedDataIndex,}) {
    bool needUpdate = false;
    if (removedDataIndexes != null && removedDataIndexes.isNotEmpty) {
      _removeDataPointsList(removedDataIndexes);
    } else if (removedDataIndex != null) {
      _removeDataPoint(removedDataIndex);
    }
    if (addedDataIndexes != null && addedDataIndexes.isNotEmpty) {
      _addOrUpdateDataPoints(addedDataIndexes, false);
    } else if (addedDataIndex != null) {
      _addOrUpdateDataPoint(addedDataIndex, false);
    }
    if (updatedDataIndexes != null && updatedDataIndexes.isNotEmpty) {
      needUpdate = true;
      _addOrUpdateDataPoints(updatedDataIndexes, true);
    } else if (updatedDataIndex != null) {
      needUpdate = true;
      _addOrUpdateDataPoint(updatedDataIndex, true);
    }
    _updateCartesianSeries(_needXRecalculation, _needYRecalculation, needUpdate);
  }

  void _addOrUpdateDataPoints(List<int> indexes, bool needUpdate) {
    for (int i = 0; i < indexes.length; i++) {
      final int dataIndex = indexes[i];
      _addOrUpdateDataPoint(dataIndex, needUpdate);
    }
  }

  void _addOrUpdateDataPoint(int index, bool needUpdate) {
    final SeriesRendererDetails seriesRendererDetails = SeriesHelper.getSeriesRendererDetails(seriesRenderer);
    final CartesianSeries<dynamic, dynamic> series = seriesRendererDetails.series;
    if (index >= 0 && series.dataSource.length > index && series.dataSource[index] != null) {
      final VisibleRange xRange = seriesRendererDetails.xAxisDetails!.visibleRange!;
      final VisibleRange yRange = seriesRendererDetails.yAxisDetails!.visibleRange!;
      final CartesianChartPoint<dynamic> currentPoint = getChartPoint(seriesRenderer, series.dataSource[index], index)!;
      final String seriesType = seriesRendererDetails.seriesType;
      dynamic x = currentPoint.x;
      if (seriesRendererDetails.xAxisDetails is DateTimeAxisDetails ||
          seriesRendererDetails.xAxisDetails is DateTimeCategoryAxisDetails) {
        x = x.millisecondsSinceEpoch;
      } else if (seriesRendererDetails.xAxisDetails is LogarithmicAxisDetails) {
        final LogarithmicAxis axis = seriesRendererDetails.xAxisDetails!.axis as LogarithmicAxis;
        currentPoint.xValue = currentPoint.x;
        x = calculateLogBaseValue((x > 1) == true ? x : 1, axis.logBase);
      } else if (seriesRendererDetails.xAxisDetails is CategoryAxisDetails) {
        final CategoryAxisDetails axisDetails = seriesRendererDetails.xAxisDetails as CategoryAxisDetails;
        final CategoryAxis categoryAxis = axisDetails.axis as CategoryAxis;
        if (categoryAxis.arrangeByIndex) {
          index < axisDetails.labels.length
              ? axisDetails.labels[index] += ', ${currentPoint.x}'
              : axisDetails.labels.add(currentPoint.x.toString());
          x = index;
        } else {
          if (!axisDetails.labels.contains(currentPoint.x.toString())) {
            axisDetails.labels.add(currentPoint.x.toString());
          }
          x = axisDetails.labels.indexOf(currentPoint.x.toString());
        }
      }
      currentPoint.xValue ??= x;
      currentPoint.yValue = currentPoint.y;
      currentPoint.overallDataPointIndex = index;
      if (series.dataSource.length == 1) {
        seriesRendererDetails.minimumX = x;
        seriesRendererDetails.maximumX = x;
      }
      if (((xRange.minimum >= x) == true || (xRange.maximum <= x) == true) && seriesRendererDetails.visible!) {
        _needXRecalculation = true;
        if (seriesRendererDetails.minimumX! >= x) {
          seriesRendererDetails.minimumX = x;
        }
        if (seriesRendererDetails.maximumX! <= x) {
          seriesRendererDetails.maximumX = x;
        }
      }
      num? minYVal = currentPoint.y ?? currentPoint.low;
      num? maxYVal = currentPoint.y ?? currentPoint.high;
      if (seriesRendererDetails.yAxisDetails is LogarithmicAxisRenderer && minYVal != null && maxYVal != null) {
        final LogarithmicAxis axis = seriesRendererDetails.yAxisDetails!.axis as LogarithmicAxis;
        minYVal = calculateLogBaseValue(minYVal > 1 ? minYVal : 1, axis.logBase);
        maxYVal = calculateLogBaseValue(maxYVal > 1 ? maxYVal : 1, axis.logBase);
      }
      if (series.dataSource.length == 1) {
        seriesRendererDetails.minimumY = minYVal;
        seriesRendererDetails.maximumY = maxYVal;
      }
      if (minYVal != null &&
          maxYVal != null &&
          ((yRange.minimum >= minYVal) == true || (yRange.maximum <= maxYVal) == true) &&
          seriesRendererDetails.visible!) {
        _needYRecalculation = true;
        if (seriesRendererDetails.minimumY! >= minYVal) {
          seriesRendererDetails.minimumY = minYVal;
        }
        if (seriesRendererDetails.maximumY! <= maxYVal) {
          seriesRendererDetails.maximumY = maxYVal;
        }
      }

      if (needUpdate) {
        if (seriesRendererDetails.dataPoints.length > index == true) {
          seriesRendererDetails.dataPoints[index] = currentPoint;
          seriesRendererDetails.overAllDataPoints[index] = currentPoint;
        }
      } else {
        if (seriesRendererDetails.dataPoints.length == index) {
          seriesRendererDetails.dataPoints.add(currentPoint);
          seriesRendererDetails.overAllDataPoints.add(currentPoint);
        } else if (seriesRendererDetails.dataPoints.length > index == true && index >= 0) {
          seriesRendererDetails.dataPoints.insert(index, currentPoint);
          seriesRendererDetails.overAllDataPoints.insert(index, currentPoint);
        }
      }

      if (seriesType.contains('range') || seriesType.contains('hilo') || seriesType.contains('candle')
          ? seriesType == 'hiloopenclose' || seriesType.contains('candle')
              ? (currentPoint.low == null ||
                  currentPoint.high == null ||
                  currentPoint.open == null ||
                  currentPoint.close == null)
              : (currentPoint.low == null || currentPoint.high == null)
          : currentPoint.y == null) {
        seriesRenderer.calculateEmptyPointValue(index, currentPoint, seriesRenderer);
      }

      if ((seriesType.contains('range') || seriesType.contains('hilo') || seriesType.contains('candle')) &&
          currentPoint.isVisible) {
        final num high = currentPoint.high;
        final num low = currentPoint.low;
        currentPoint.high = math.max<num>(high, low);
        currentPoint.low = math.min<num>(high, low);
      }
    }
  }

  CartesianChartPoint<dynamic> pixelToPoint(Offset position) {
    return calculatePixelToPoint(position, seriesRenderer);
  }

  Offset pointToPixel(CartesianChartPoint<dynamic> point) {
    return calculatePointToPixel(point, seriesRenderer);
  }

  void animate() {
    final SeriesRendererDetails seriesRendererDetails = SeriesHelper.getSeriesRendererDetails(seriesRenderer);
    if (seriesRendererDetails.visible! == true && seriesRendererDetails.series.animationDuration > 0 == true) {
      final CartesianStateProperties stateProperties = seriesRendererDetails.stateProperties;
      final CgCartesianChart chart = seriesRendererDetails.stateProperties.chart;
      final TooltipBehavior tooltip = chart.tooltipBehavior;
      final TrackballBehavior trackball = chart.trackballBehavior;
      final TrackballRenderingDetails trackballRenderingDetails =
          TrackballHelper.getRenderingDetails(stateProperties.trackballBehaviorRenderer);
      final TrackballPainter? trackballPainter = trackballRenderingDetails.trackballPainter;
      final RenderingDetails renderingDetails = stateProperties.renderingDetails;
      final TooltipRenderingDetails tooltipRenderingDetails =
          TooltipHelper.getRenderingDetails(renderingDetails.tooltipBehaviorRenderer);
      if (tooltip.enable &&
          (tooltip.builder != null
              ? tooltipRenderingDetails.seriesIndex ==
                  SegmentHelper.getSegmentProperties(seriesRendererDetails.segments[0]).seriesIndex
              : tooltipRenderingDetails.currentSeriesDetails?.renderer == seriesRenderer)) {
        tooltip.hide();
      }
      if (trackball.enable) {
        for (final ChartPointInfo point in trackballRenderingDetails.chartPointInfo) {
          if (point.seriesRendererDetails?.renderer == seriesRenderer) {
            if (trackballPainter != null) {
              stateProperties.repaintNotifiers['trackball']!.value++;
              trackballPainter.canResetPath = true;
              break;
            } else {
              final GlobalKey key = trackballRenderingDetails.trackballTemplate!.key as GlobalKey;
              final TrackballTemplateState trackballTemplateState = key.currentState! as TrackballTemplateState;
              trackballTemplateState.hideTrackballTemplate();
              break;
            }
          }
        }
      }
      seriesRendererDetails.reAnimate =
          seriesRendererDetails.needsAnimation = seriesRendererDetails.needAnimateSeriesElements = true;
      renderingDetails.initialRender = false;
      stateProperties.renderDataLabel?.state?.repaintDataLabelElements();

      if (seriesRendererDetails.series.dataLabelSettings.builder != null) {
        for (final ChartTemplateInfo template in renderingDetails.templates) {
          if (template.templateType == 'DataLabel' &&
              template.animationDuration > 0 &&
              template.seriesIndex ==
                  SegmentHelper.getSegmentProperties(seriesRendererDetails.segments[0]).seriesIndex) {
            template.animationController.forward(from: 0);
          }
        }
      }
      stateProperties.totalAnimatingSeries = 1;
      stateProperties.animationCompleteCount = 0;
      stateProperties.forwardAnimation(seriesRendererDetails);
      if (seriesRendererDetails.trendlineRenderer.isNotEmpty == true) {
        for (final TrendlineRenderer trendlineRenderer in seriesRendererDetails.trendlineRenderer) {
          if (trendlineRenderer.visible) {
            final Trendline trendline = trendlineRenderer.trendline;
            trendlineRenderer.animationController.duration =
                Duration(milliseconds: trendline.animationDuration.toInt());
            trendlineRenderer.animationController.forward(from: 0);
          }
        }
      }
    }
  }

  void _removeDataPointsList(List<int> removedDataIndexes) {
    final List<int> indexList = removedDataIndexes.toSet().toList();
    indexList.sort((b, a) => a.compareTo(b));
    for (int i = 0; i < indexList.length; i++) {
      final int dataIndex = indexList[i];
      _removeDataPoint(dataIndex);
    }
  }

  void _removeDataPoint(int index) {
    final SeriesRendererDetails seriesRendererDetails = SeriesHelper.getSeriesRendererDetails(seriesRenderer);
    if (seriesRendererDetails.dataPoints.isNotEmpty == true &&
        index >= 0 &&
        index < seriesRendererDetails.dataPoints.length) {
      final CartesianChartPoint<dynamic> currentPoint = seriesRendererDetails.dataPoints[index];
      final ChartAxisRendererDetails xAxisDetails = seriesRendererDetails.xAxisDetails!;
      if (xAxisDetails is DateTimeCategoryAxisRenderer || xAxisDetails is CategoryAxisRenderer) {
        _needXRecalculation = true;
      }
      seriesRendererDetails.dataPoints.removeAt(index);
      seriesRendererDetails.overAllDataPoints.removeAt(index);
      if (!_needXRecalculation &&
          (seriesRendererDetails.minimumX == currentPoint.xValue ||
              seriesRendererDetails.maximumX == currentPoint.xValue)) {
        _needXRecalculation = true;
        _needRemove = true;
      }
      final String seriesType = seriesRendererDetails.seriesType;

      if ((seriesType.contains('range') || seriesType.contains('hilo') || seriesType.contains('candle')) &&
          currentPoint.isVisible) {
        final num high = currentPoint.high;
        final num low = currentPoint.low;
        currentPoint.high = math.max<num>(high, low);
        currentPoint.low = math.min<num>(high, low);
      }
      final num? minYVal = currentPoint.y ?? currentPoint.low;
      final num? maxYVal = currentPoint.y ?? currentPoint.high;
      if (!_needYRecalculation &&
          minYVal != null &&
          maxYVal != null &&
          (seriesRendererDetails.minimumY == minYVal || seriesRendererDetails.maximumY == maxYVal)) {
        _needYRecalculation = true;
        _needRemove = true;
      }
    }
  }

  void _updateCartesianSeries(bool needXRecalculation, bool needYRecalculation, bool needUpdate) {
    final SeriesRendererDetails seriesRendererDetails = SeriesHelper.getSeriesRendererDetails(seriesRenderer);
    final CartesianStateProperties stateProperties = seriesRendererDetails.stateProperties;
    final ChartAxisRendererDetails xAxisDetails = seriesRendererDetails.xAxisDetails!;
    final String seriesType = seriesRendererDetails.seriesType;
    final bool isFindSeriesMinMax = xAxisDetails is DateTimeCategoryAxisDetails ||
        seriesType == 'boxandwhisker' ||
        seriesType == 'waterfall' ||
        seriesType == 'errorbar' ||
        seriesRendererDetails.seriesType.contains('stacked') == true;
    stateProperties.isRedrawByZoomPan = false;
    if (needXRecalculation || needYRecalculation || needUpdate) {
      if (isFindSeriesMinMax || _needRemove) {
        if (needXRecalculation) {
          seriesRendererDetails.minimumX = seriesRendererDetails.maximumX = null;
          if (xAxisDetails is DateTimeCategoryAxisDetails) {
            xAxisDetails.labels.clear();
          }
        }
        if (needYRecalculation) {
          seriesRendererDetails.minimumY = seriesRendererDetails.maximumY = null;
        }
        stateProperties.chartSeries.findSeriesMinMax(seriesRendererDetails);
      }
    }
    if (needXRecalculation) {
      final dynamic axisRenderer = seriesRendererDetails.xAxisDetails;
      axisRenderer.calculateRangeAndInterval(stateProperties);
    }
    if (needYRecalculation) {
      final dynamic axisRenderer = seriesRendererDetails.yAxisDetails;
      axisRenderer.calculateRangeAndInterval(stateProperties);
    }
    if (needXRecalculation || needYRecalculation) {
      stateProperties.plotBandRepaintNotifier.value++;
      stateProperties.renderOutsideAxis.state.axisRepaintNotifier.value++;
      stateProperties.renderInsideAxis.state.axisRepaintNotifier.value++;
      for (final CartesianSeriesRenderer seriesRenderer in stateProperties.chartSeries.visibleSeriesRenderers) {
        _repaintSeries(stateProperties, SeriesHelper.getSeriesRendererDetails(seriesRenderer));
      }
    } else {
      _repaintSeries(stateProperties, seriesRendererDetails);
    }
    stateProperties.isLoadMoreIndicator = false;
    if (!isFindSeriesMinMax) {
      _needXRecalculation = false;
      _needYRecalculation = false;
    }
  }

  void _repaintSeries(CartesianStateProperties stateProperties, SeriesRendererDetails seriesRendererDetails) {
    seriesRendererDetails.calculateRegion = true;
    seriesRendererDetails.repaintNotifier.value++;
    if (seriesRendererDetails.series.dataLabelSettings.isVisible == true) {
      stateProperties.renderDataLabel?.state!.dataLabelRepaintNotifier.value++;
    }
  }
}

abstract class CartesianSeriesRenderer extends ChartSeriesRenderer {
  late SeriesRendererDetails _seriesRendererDetails;

  ChartSegment createSegment();

  void customizeSegment(ChartSegment segment);

  void drawDataMarker(int index, Canvas canvas, Paint fillPaint, Paint strokePaint, double pointX, double pointY,
      [CartesianSeriesRenderer seriesRenderer,]);

  void drawDataLabel(
      int index, Canvas canvas, String dataLabel, double pointX, double pointY, int angle, TextStyle style,);

  void calculateEmptyPointValue(int pointIndex, CartesianChartPoint<dynamic> currentPoint,
      [CartesianSeriesRenderer seriesRenderer,]);

  void dispose() {
    _seriesRendererDetails.dispose();
  }
}

class SeriesHelper {
  const SeriesHelper._();

  static SeriesRendererDetails getSeriesRendererDetails(CartesianSeriesRenderer renderer) =>
      renderer._seriesRendererDetails;

  static void setSeriesRendererDetails(CartesianSeriesRenderer renderer, SeriesRendererDetails rendererDetails) =>
      renderer._seriesRendererDetails = rendererDetails;
}
